<!--
 * @Description: 流程设计器
 * @Author: DHL
 * @Date: 2022-04-18 16:41:25
 * @LastEditors: DHL
 * @LastEditTime: 2022-10-08 13:26:37
-->

<template>
  <div class="process-designer-wrapper">
    <div class="process-designer__header">
      <!-- 用于打开本地文件-->
      <input
        type="file"
        id="files"
        ref="openFileRef"
        style="display: none"
        accept=".xml, .bpmn"
        @change="handleImportLocalFile"
      />

      <el-button-group>
        <el-button type="primary" @click="openFileRef.click()">打开文件</el-button>
        <el-popover
          placement="bottom-start"
          :width="130"
          trigger="hover"
          popper-class="process-designer"
        >
          <el-link type="primary" @click="handleDownloadProcessAsXml">下载为XML文件</el-link>
          <el-link type="primary" @click="handleDownloadProcessAsSvg">下载为SVG文件</el-link>
          <el-link type="primary" @click="handleDownloadProcessAsBpmn">下载为BPMN文件</el-link>
          <template #reference>
            <el-button type="primary">下载文件</el-button>
          </template>
        </el-popover>

        <el-popover
          placement="bottom-start"
          :width="90"
          trigger="hover"
          popper-class="process-designer"
        >
          <el-link type="primary" @click="handlePreviewProcessXML">预览XML</el-link>
          <el-link type="primary" @click="handlePreviewProcessJson">预览JSON</el-link>
          <template #reference>
            <el-button type="primary">预览</el-button>
          </template>
        </el-popover>

        <el-button
          type="primary"
          v-if="simulation"
          :title="simulationStatus ? '退出模拟' : '开启模拟'"
          @click="handleProcessSimulation"
        >
          模拟
        </el-button>
      </el-button-group>

      <el-button-group class="ml10">
        <el-button plain title="向左对齐" @click="handleElementsAlign('left')">
          <svg-icon iconClass="align" className="align align-left"></svg-icon>
        </el-button>
        <el-button plain title="向右对齐" @click="handleElementsAlign('right')">
          <svg-icon iconClass="align" className="align align-right"></svg-icon>
        </el-button>
        <el-button plain title="向上对齐" @click="handleElementsAlign('top')">
          <svg-icon iconClass="align" className="align align-top"></svg-icon>
        </el-button>
        <el-button plain title="向下对齐" @click="handleElementsAlign('bottom')">
          <svg-icon iconClass="align"></svg-icon>
        </el-button>
        <el-button plain title="水平居中" @click="handleElementsAlign('center')">
          <svg-icon iconClass="align-center"></svg-icon>
        </el-button>
        <el-button plain title="垂直居中" @click="handleElementsAlign('middle')">
          <svg-icon iconClass="align-center" className="align align-vertical"></svg-icon>
        </el-button>
        <el-button plain title="水平分布" @click="handleElementsDistribute('horizontal')">
          <svg-icon iconClass="align-distribute" className="align align-horizontal"></svg-icon>
        </el-button>
        <el-button plain title="垂直分布" @click="handleElementsDistribute('vertical')">
          <svg-icon iconClass="align-distribute"></svg-icon>
        </el-button>
      </el-button-group>

      <el-button-group class="ml10">
        <el-button
          plain
          title="缩小视图"
          :disabled="defaultZoom < 0.2"
          @click="handleProcessZoomOut()"
        >
          <svg-icon iconClass="zoomOut"></svg-icon>
        </el-button>
        <el-button plain type="primary">
          {{ Math.floor(defaultZoom * 10 * 10) + '%' }}
        </el-button>
        <el-button
          plain
          title="放大视图"
          :disabled="defaultZoom > 4"
          @click="handleProcessZoomIn()"
        >
          <svg-icon iconClass="zoomIn"></svg-icon>
        </el-button>
        <el-button plain title="重置视图并居中" @click="handleProcessReZoom()">
          <svg-icon iconClass="picCenter"></svg-icon>
        </el-button>
      </el-button-group>

      <el-button-group class="ml10">
        <el-button plain title="撤销" :disabled="!revocable" @click="handleProcessUndo">
          <svg-icon iconClass="repeal"></svg-icon>
        </el-button>
        <el-button plain title="恢复" :disabled="!recoverable" @click="handleProcessRedo">
          <svg-icon iconClass="repeal" className="flip-horizontal"></svg-icon>
        </el-button>
        <el-button plain title="重新绘制" @click="handleProcessRestart">
          <svg-icon iconClass="refresh"></svg-icon>
        </el-button>
      </el-button-group>

      <el-button-group class="ml10">
        <el-button type="primary" @click="handleSave()">保存</el-button>
      </el-button-group>
    </div>
    <div class="process-designer__container">
      <div class="process-designer-canvas" ref="bpmnCanvasRef"></div>
    </div>

    <ProcessPreview ref="processPreviewRef"></ProcessPreview>
  </div>
</template>

<script lang="ts">
  export default { name: 'ProcessDesigner' }
</script>

<script setup lang="ts">
  import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
  import { log } from '/@/utils/log'
  import { ElMessage, ElMessageBox } from 'element-plus'

  import DefaultEmptyXML from '../resources/defaultEmpty'

  // 翻译
  import translationsCN from '../translate/zh'
  import customTranslate from '../translate/customTranslate'

  // 模拟流转流程
  import tokenSimulation from 'bpmn-js-token-simulation'

  // 标签解析 Moddle
  import flowableModdleDescriptor from '../descriptor/flowableDescriptor.json'

  // 标签解析 Extension
  import flowableModdleExtension from '../extension-moddle/flowable/flowableExtension'

  // 引入json转换与高亮
  import X2JS from 'x2js'

  // 小地图
  import minimapModule from 'diagram-js-minimap'

  // 任务resize
  import resizeTask from 'bpmn-js-task-resize/lib'

  // 预览
  import ProcessPreview from './processPreview.vue'

  // 自定义contextPad
  import EnhancementContextPad from '../additional-modules/contextPad/enhancementContextPad/index'

  import { useBpmnStore } from '../store/bpmn'
  import { assign } from 'lodash-es'

  //////////////////////////////////////////////////
  // 属性
  //////////////////////////////////////////////////

  const bpmnStore = useBpmnStore()

  /**
   * 画布
   */
  const bpmnCanvasRef = ref()

  /**
   * 打开本地文件
   */
  const openFileRef = ref()

  /**
   * 预览
   */
  const processPreviewRef = ref()

  /**
   *
   * 放大缩小
   */
  const defaultZoom = ref(1)

  /**
   * 撤销
   */
  const revocable = ref(false)

  /**
   * 恢复
   */
  const recoverable = ref(false)

  const simulationStatus = ref(false)

  const state = reactive({
    xml: '', // xml 字符串
    processId: '',
    processName: '',
    category: '', // 分类
    description: ''
  })

  //////////////////////////////////////////////////
  // 参数
  //////////////////////////////////////////////////
  interface Props {
    translations?: Object // 自定义的翻译文件
    options?: Object
    additionalModel?: [Object, Array<any>] // 自定义model
    moddleExtension?: Object // 自定义moddle
    onlyCustomizeAddi?: boolean
    onlyCustomizeModdle?: boolean
    keyboard?: boolean
    simulation?: boolean
    prefix?: string
  }

  const props = withDefaults(defineProps<Props>(), {
    options: () => ({}),
    onlyCustomizeAddi: false,
    onlyCustomizeModdle: false,
    keyboard: false,
    simulation: true,
    prefix: 'flowable'
  })

  const emit = defineEmits([
    'init-finished',
    'commandStack-changed',
    'input',
    'change',
    'canvas-viewbox-changed',
    'save'
  ])

  //////////////////////////////////////////////////
  // 计算
  //////////////////////////////////////////////////
  /**
   * 添加模块
   */
  const additionalModules = computed(() => {
    const Modules: any[] = []

    // 仅保留用户自定义扩展模块
    if (props.onlyCustomizeAddi) {
      if (Object.prototype.toString.call(props.additionalModel) === '[object Array]') {
        return props.additionalModel || []
      }
      return [props.additionalModel]
    }

    // 插入用户自定义扩展模块
    if (
      Object.prototype.toString.call(props.additionalModel) === '[object Array]' &&
      props.additionalModel
    ) {
      Modules.push(...props.additionalModel)
    } else {
      props.additionalModel && Modules.push(props.additionalModel)
    }

    // 翻译模块
    const TranslateModule = {
      translate: ['value', customTranslate(props.translations || translationsCN)]
    }
    Modules.push(TranslateModule)

    // 模拟流转模块
    if (props.simulation) {
      Modules.push(tokenSimulation)
    }

    // 小地图
    Modules.push(minimapModule)
    Modules.push(resizeTask)

    // 根据需要的流程类型设置扩展元素构建模块
    if (props.prefix === 'flowable') {
      Modules.push(flowableModdleExtension)
    }

    // 自定义contextPad
    Modules.push(EnhancementContextPad)

    return Modules
  })

  /**
   * 模块扩展
   */
  const moddleExtensions = computed(() => {
    const Extensions: any = {}
    // 仅使用用户自定义模块
    if (props.onlyCustomizeModdle) {
      return props.moddleExtension || null
    }

    // 插入用户自定义模块
    if (props.moddleExtension) {
      const keys: any[] = Object.keys(props.moddleExtension)
      keys.forEach((key) => {
        Extensions[key] = key.value
      })
    }

    // 根据需要的 "流程类型" 设置 对应的解析文件
    if (props.prefix === 'flowable') {
      Extensions.flowable = flowableModdleDescriptor
    }

    return Extensions
  })

  //////////////////////////////////////////////////
  // 函数
  //////////////////////////////////////////////////

  /**
   * 初始化
   */
  function initBpmnModeler() {
    bpmnStore.buildBpmnModeler({
      container: bpmnCanvasRef.value,
      keyboard: props.keyboard ? { bindTo: document } : null,
      additionalModules: additionalModules.value,
      moddleExtensions: moddleExtensions.value,
      ...props.options,
      taskResizingEnabled: true,
      eventResizingEnabled: true
    })

    initModelListeners()
  }

  /**
   * 初始化监听
   */
  function initModelListeners() {
    const EventBus = bpmnStore.bpmnModeler.get('eventBus')

    // 注册需要的监听事件, 将. 替换为 - , 避免解析异常
    // props.events.forEach((event) => {
    //   EventBus.on(event, function (eventObj: { element: any }) {
    //     let eventName = event.replace(/\./g, '-')
    //     let element = eventObj ? eventObj.element : null
    //     emit(eventName, element, eventObj)
    //   })
    // })

    // 监听图形改变返回xml
    EventBus.on('commandStack.changed', async (event: any) => {
      try {
        recoverable.value = bpmnStore.bpmnModeler.get('commandStack').canRedo()
        revocable.value = bpmnStore.bpmnModeler.get('commandStack').canUndo()
        let { xml } = await bpmnStore.bpmnModeler.saveXML({ format: true })
        emit('commandStack-changed', event)
        emit('input', xml)
        emit('change', xml)
      } catch (e: any) {
        log.error('Process Designer Warn', e.message || e)
      }
    })

    // 监听视图缩放变化
    bpmnStore.bpmnModeler.on('canvas.viewbox.changed', ({ viewbox }: any) => {
      emit('canvas-viewbox-changed', { viewbox })
      const { scale } = viewbox
      defaultZoom.value = Math.floor(scale * 100) / 100
    })
  }

  /**
   * 创建新的流程图
   */
  async function createNewDiagram(xml: Nullable<string | ArrayBuffer>) {
    // 将字符串转换成图显示出来
    let newId = state.processId || `Process_${new Date().getTime()}`
    let newName = state.processName || `业务流程_${new Date().getTime()}`
    let category = state.category || ''
    let description = state.description || ''
    let xmlString = xml || DefaultEmptyXML(newId, newName, category, description)
    try {
      let { warnings } = await bpmnStore.bpmnModeler.importXML(xmlString)
      if (warnings && warnings.length > 0) {
        warnings.forEach((warn: any) => log.warn('创建新的流程图', warn))
      }
    } catch (e: any) {
      log.error('Process Designer Warn', e?.message || e)
    }

    emit('init-finished')
  }

  /**
   * 下载流程图到本地
   */
  async function downloadProcess(type: string, name = 'diagram') {
    try {
      // 按需要类型创建文件并下载
      if (type === 'xml' || type === 'bpmn') {
        const { err, xml } = await bpmnStore.bpmnModeler.saveXML()
        // 读取异常时抛出异常
        if (err) {
          log.error('Process Designer Warn', err.message || err)
        }
        let { href, filename } = setEncoded(type.toUpperCase(), name, xml)
        downloadFunc(href, filename)
      } else {
        const { err, svg } = await bpmnStore.bpmnModeler.saveSVG()
        // 读取异常时抛出异常
        if (err) {
          return console.error(err)
        }
        let { href, filename } = setEncoded('SVG', name, svg)
        downloadFunc(href, filename)
      }
    } catch (e: any) {
      log.error('Process Designer Warn', e.message || e)
    }
    // 文件下载方法
    function downloadFunc(href: string, filename: string) {
      if (href && filename) {
        let a = document.createElement('a')
        a.download = filename //指定下载的文件名
        a.href = href //  URL对象
        a.click() // 模拟点击
        URL.revokeObjectURL(a.href) // 释放URL 对象
      }
    }
  }

  /**
   * 根据所需类型进行转码并返回下载地址
   */
  function setEncoded(type: string, filename = 'diagram', data: string) {
    const encodedData = encodeURIComponent(data)
    return {
      filename: `${filename}.${type}`,
      href: `data:application/${
        type === 'svg' ? 'text/xml' : 'bpmn20-xml'
      };charset=UTF-8,${encodedData}`,
      data: data
    }
  }

  /* =========================== 方法开始 =========================== */

  /**
   * 加载本地文件
   */
  function handleImportLocalFile() {
    const file = openFileRef.value.files[0]
    const reader = new FileReader()
    reader.readAsText(file)
    reader.onload = function () {
      let xmlStr = this.result
      createNewDiagram(xmlStr)
    }
  }

  function handleDownloadProcessAsXml() {
    downloadProcess('xml')
  }

  function handleDownloadProcessAsBpmn() {
    downloadProcess('bpmn')
  }

  function handleDownloadProcessAsSvg() {
    downloadProcess('svg')
  }

  /**
   * 对齐
   * @param align
   */
  function handleElementsAlign(align: string) {
    const Align = bpmnStore.bpmnModeler.get('alignElements')
    const Selection = bpmnStore.bpmnModeler.get('selection')
    const SelectedElements = Selection.get()

    if (!SelectedElements || SelectedElements.length <= 1) {
      ElMessage.warning('请按住 Shift 键选择多个元素对齐')
      return
    }

    ElMessageBox.confirm('自动对齐可能造成图形变形，是否继续？', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => Align.trigger(SelectedElements, align))
  }

  /**
   * 分布
   * @param direction
   */
  function handleElementsDistribute(direction: string) {
    const Distribute = bpmnStore.bpmnModeler.get('distributeElements')
    const Selection = bpmnStore.bpmnModeler.get('selection')
    const SelectedElements = Selection.get()

    if (!SelectedElements || SelectedElements.length <= 1) {
      ElMessage.warning('请按住 Shift 键选择多个元素对齐')
      return
    }

    ElMessageBox.confirm('自动分布可能造成图形变形，是否继续？', '警告', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      type: 'warning'
    }).then(() => Distribute.trigger(SelectedElements, direction))
  }

  /**
   * 缩小视图
   */
  function handleProcessZoomOut(zoomStep = 0.1) {
    let newZoom = Math.floor(defaultZoom.value * 100 - zoomStep * 100) / 100
    if (newZoom < 0.2) {
      throw new Error('[Process Designer Warn ]: The zoom ratio cannot be less than 0.2')
    }
    defaultZoom.value = newZoom
    bpmnStore.bpmnModeler.get('canvas').zoom(defaultZoom.value)
  }

  /**
   * 放大视图
   * @param zoomStep
   */
  function handleProcessZoomIn(zoomStep = 0.1) {
    let newZoom = Math.floor(defaultZoom.value * 100 + zoomStep * 100) / 100
    if (newZoom > 4) {
      throw new Error('[Process Designer Warn ]: The zoom ratio cannot be greater than 4')
    }
    defaultZoom.value = newZoom
    bpmnStore.bpmnModeler.get('canvas').zoom(defaultZoom.value)
  }

  /**
   * 重置视图并居中
   */
  function handleProcessReZoom() {
    defaultZoom.value = 1
    bpmnStore.bpmnModeler.get('canvas').zoom('fit-viewport', 'auto')
  }

  /**
   * 撤销
   */
  function handleProcessUndo() {
    bpmnStore.bpmnModeler.get('commandStack').undo()
  }

  /**
   * 恢复
   */
  function handleProcessRedo() {
    bpmnStore.bpmnModeler.get('commandStack').redo()
  }

  /**
   * 重新绘制
   */
  function handleProcessRestart() {
    recoverable.value = false
    revocable.value = false
    createNewDiagram(null)
  }

  /**
   * 预览xml
   */
  function handlePreviewProcessXML() {
    bpmnStore.bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
      processPreviewRef.value.handleOpen('xml', xml)
    })
  }

  /**
   * 预览json
   */
  function handlePreviewProcessJson() {
    const newConvert = new X2JS()

    bpmnStore.bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
      const { definitions } = newConvert.xml2js<{ definitions: object }>(xml)
      let previewResult = ''
      if (definitions) {
        previewResult = JSON.stringify(definitions, null, 4)
      }
      processPreviewRef.value.handleOpen('json', previewResult)
    })
  }

  /**
   * 流程模拟
   */
  function handleProcessSimulation() {
    simulationStatus.value = !simulationStatus.value
    simulationStatus.value && bpmnStore.bpmnModeler.get('toggleMode').toggleMode()
  }

  /* =========================== 方法结束 =========================== */

  //////////////////////////////////////////////////
  // 保存模型
  //////////////////////////////////////////////////

  /**
   * 保存流程模型
   */
  function handleSave() {
    bpmnStore.bpmnModeler.saveXML({ format: true }).then(({ xml }: { xml: string }) => {
      emit('save', { xml: xml })
    })
  }

  //////////////////////////////////////////////////
  // 生命周期
  //////////////////////////////////////////////////
  // onMounted(() => {
  //   nextTick(() => {
  //     initBpmnModeler()
  //     createNewDiagram(props.value)
  //   })
  // })

  /**
   * 初始化
   */
  function handleInit(params: any) {
    bpmnStore.destroy()

    setTimeout(() => {
      assign(state, {
        xml: params.editor, // xml 字符串
        processId: params.key,
        processName: params.name,
        category: params.category,
        description: params.description
      })

      nextTick(() => {
        initBpmnModeler()
        createNewDiagram(state.xml)
      })
    }, 200)
  }

  onBeforeUnmount(() => {
    bpmnStore.destroy()
  })

  //////////////////////////////////////////////////
  // 暴露出去，父组件才可以调用到
  //////////////////////////////////////////////////

  defineExpose({
    handleInit
  })
</script>
